// JCL_DEBUG_EXPERT_GENERATEJDBG OFF
program Quake3Explorer;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Math,
  SampleCommon,
  DIrr in '..\..\DSource\DIrr.pas',
  DIrr.Irr in '..\..\DSource\DIrr.Irr.pas',
  DIrr.GUI in '..\..\DSource\DIrr.GUI.pas',
  DIrr.Color in '..\..\DSource\DIrr.Color.pas',
  DIrr.Rect in '..\..\DSource\DIrr.Rect.pas',
  DIrr.Event in '..\..\DSource\DIrr.Event.pas',
  DIrr.Vector2D in '..\..\DSource\DIrr.Vector2D.pas',
  DIrr.Vector3D in '..\..\DSource\DIrr.Vector3D.pas',
  DIrr.Video in '..\..\DSource\DIrr.Video.pas',
  DIrr.Base in '..\..\DSource\DIrr.Base.pas',
  DIrr.Types in '..\..\DSource\DIrr.Types.pas',
  DIrr.Scene in '..\..\DSource\DIrr.Scene.pas',
  DIrr.Structs in '..\..\DSource\DIrr.Structs.pas',
  DIrr.Aabbox3D in '..\..\DSource\DIrr.Aabbox3D.pas',
  DIrr.Matrix4 in '..\..\DSource\DIrr.Matrix4.pas',
  DIrr.Texture in '..\..\DSource\DIrr.Texture.pas',
  DIrr.Dimension2D in '..\..\DSource\DIrr.Dimension2D.pas',
  DIrr.Collections in '..\..\DSource\DIrr.Collections.pas',
  DIrr.ConcreteMesh in '..\..\DSource\DIrr.ConcreteMesh.pas',
  DIrr.Quake3 in '..\..\DSource\DIrr.Quake3.pas',
  DIrr.Buffer in '..\..\DSource\DIrr.Buffer.pas',
  DIrr.IO in '..\..\DSource\DIrr.IO.pas',
  DIrr.Line3D in '..\..\DSource\DIrr.Line3D.pas',
  DIrr.Plane3D in '..\..\DSource\DIrr.Plane3D.pas',
  DIrr.Math in '..\..\DSource\DIrr.Math.pas',
  DIrr.Triangle3D in '..\..\DSource\DIrr.Triangle3D.pas',
  DIrr.Consts in '..\..\DSource\DIrr.Consts.pas',
  DIrr.Q3Shader in '..\..\DSource\DIrr.Q3Shader.pas',
  System.Generics.Defaults,
  System.Generics.Collections,
  Sound in 'Sound.pas',
  Q3Factory in 'Q3Factory.pas';

(** Example 021 Quake3 Explorer

This Tutorial shows how to load different Quake 3 maps.

Features:
	- Load BSP Archives at Runtime from the menu
	- Load a Map from the menu. Showing with Screenshot
	- Set the VideoDriver at runtime from menu
	- Adjust GammaLevel at runtime
	- Create SceneNodes for the Shaders
	- Load EntityList and create Entity SceneNodes
	- Create Players with Weapons and with Collision Response
	- Play music

You can download the Quake III Arena demo ( copyright id software )
at the following location:
ftp://ftp.idsoftware.com/idstuff/quake3/win32/q3ademo.exe

Copyright 2006-2011 Burningwater, Thomas Alten
*)

type
  TGUI = record
  public
    VideoDriver: TGUIComboBox;
    VideoMode: TGUIComboBox;
    FullScreen: TGUICheckBox;
    Bit32: TGUICheckBox;
    MultiSample: TGUIScrollBar;
    SetVideoMode: TGUIButton;

    Tesselation: TGUIScrollBar;
    Gamma: TGUIScrollBar;
    Collision: TGUICheckBox;
    Visible_Map: TGUICheckBOx;
    Visible_Shader: TGUICheckBox;
    Visible_Fog: TGUICheckBox;
    Visible_Unresolved: TGUICheckBox;
    Visible_Skydome: TGUICheckBox;
    Respawn: TGUIButton;

    ArchiveList: TGUITable;
    ArchiveAdd: TGUIButton;
    ArchiveRemove: TGUIButton;
    ArchiveFileOpen: TGUIFileOpenDialog;
    ArchiveUp: TGUIButton;
    ArchiveDown: TGUIButton;
    MapList: TGUIListBox;
    SceneTree: TGUITreeView;
    Logo: TGUIImage;
    Window: TGUIWindow;
  public
    procedure Init;
    procedure Drop;
  end;

type
  TArchiveList = array of AnsiString;
  TArchiveListHelper = record helper for TArchiveList
  private
    function GetCount: Integer; inline;
  public
    procedure Clear;
    procedure PushBack(const NewStr: AnsiString);
  public
    property Count: Integer read GetCount;
  end;

  PGameData = ^TGameData;
  TGameData = record
  public
    procedure Init(const AStartupDir: AnsiString = '');
    procedure SetDefault;
    function Save(const FileName: AnsiString): S32;
    function Load(const FileName: AnsiString): S32;
  public
    DebugState: E_DEBUG_SCENE_TYPES;
    GravityState: S32;
    FlyTroughState: S32;
    WireFrame: S32;
    GuiActive: S32;
    GuiInputActive: S32;
    GammaValue: F32;
    RetVal: S32;
    Sound: S32;

    StartupDir: AnsiString;
    CurrentMapName: AnsiString;
    CurrentArchiveList: TArchiveList;

    PlayerPosition: TVector3DF;
    PlayerRotation: TVector3DF;

    Variable: TQ3EntityList;

    LoadParam: TQ3LevelLoadParameter;
    DeviceParam: SIrrlichtCreationParameters;
    CreateExDevice: TCreateDeviceEx_Fun;
    Device: TIrrlichtDevice;
  end;

(*
    Normally, you are limited to 8 dynamic lights per scene: this is a hardware limit.  If you
    want to use more dynamic lights in your scene, then you can register an optional light
    manager that allows you to to turn lights on and off at specific point during rendering.
    You are still limited to 8 lights, but the limit is per scene node.

    This is completely optional: if you do not register a light manager, then a default
    distance-based scheme will be used to prioritise hardware lights based on their distance
    from the active camera.

	NO_MANAGEMENT disables the light manager and shows Irrlicht's default light behaviour.
    The 8 lights nearest to the camera will be turned on, and other lights will be turned off.
    In this example, this produces a funky looking but incoherent light display.

	LIGHTS_NEAREST_NODE shows an implementation that turns on a limited number of lights
    per mesh scene node.  If finds the 3 lights that are nearest to the node being rendered,
    and turns them on, turning all other lights off.  This works, but as it operates on every
    light for every node, it does not scale well with many lights.  The flickering you can see
    in this demo is due to the lights swapping their relative positions from the cubes
    (a deliberate demonstration of the limitations of this technique).

	LIGHTS_IN_ZONE shows a technique for turning on lights based on a 'zone'. Each empty scene
    node is considered to be the parent of a zone.  When nodes are rendered, they turn off all
    lights, then find their parent 'zone' and turn on all lights that are inside that zone, i.e.
	are  descendents of it in the scene graph.  This produces true 'local' lighting for each cube
    in this example.  You could use a similar technique to locally light all meshes in (e.g.)
    a room, without the lights spilling out to other rooms.

	This light manager is also an event receiver; this is purely for simplicity in this example,
    it's neither necessary nor recommended for a real application.
*)
type
  TMyLightManager = class(TLightManager)
  private type
    TLightManagementMode = (lmmNoManagement, lmmLightsNearestNode, lmmLightsInZone);

    // A utility class to aid in sorting scene nodes into a distance order
    TLightDistanceElement = class
    private
      FNode: TSceneNode;
      FDistance: F64;
    private type
      TLDEComparer = class(TInterfacedObject, IComparer<TLightDistanceElement>)
      private
        function Compare(const Left, Right: TLightDistanceElement): Integer;
      end;
    public
      constructor Create; overload;
      constructor Create(Node: TSceneNode; d: F64); overload;
    public
      class function GetComparer: IComparer<TLightDistanceElement>; inline; static;
    public
      property Distance: F64 read FDistance write FDistance;
      property ConcreteNode: TSceneNode read FNode write FNode;
    end;

  private
    FMode: TLightManagementMode;
    FRequestedMode: TLightManagementMode;

    // These data represent the state information that this light manager
    // is interested in.
    FSceneManager: TSceneManager;

    FSceneLightListWrap: TSceneNodeArray;

    FCurrentRenderPass: E_SCENE_NODE_RENDER_PASS;
    FCurrentSceneNode: TSceneNode;
  private
    function FindZone(ANode: TSceneNode): TSceneNode;
    procedure TurnOnZoneLights(ANode: TSceneNode);
  public
    constructor Create(SceneMgr: TSceneManager);

    // The input receiver interface, which just switches light management strategy
    function OnEvent(const Event: SEvent): Boolean; override;
  public
    procedure OnPreRender(LightList: TSceneNodeArray); override;
    procedure OnPostRender; override;
    procedure OnRenderPassPreRender(RenderPass: E_SCENE_NODE_RENDER_PASS); override;
    procedure OnRenderPassPostRender(RenderPass: E_SCENE_NODE_RENDER_PASS); override;
    procedure OnNodePreRender(ANode: TSceneNode); override;
    procedure OnNodePostRender(ANode: TSceneNode); override;
  end;

  (*
	  Representing a player
  *)
  TQ3Player = class(TAnimatedMeshSceneNode)
  private
    FDevice: TIrrlichtDevice;
    FMapParent: TSceneNode;
    FMesh: TQ3LevelMesh;
    FWeaponNode: TAnimatedMeshSceneNode;
    FStartPositionCurrent: S32;
    FAnim: array[0..3] of TTimeFire;
    FAnimation: array[0..63] of C8;
    FBuf: array[0..63] of C8;
  public
    procedure OnAnimationEnd(Node: TAnimatedMeshSceneNode); virtual;
    procedure CreateIt(ADevice: TIrrlichtDevice; AMesh: TQ3LevelMesh;
      AMapNode: TSceneNode; AMeta: TMetaTriangleSelector);
    procedure ShutDown;
    procedure SetAnim(const Name: PC8);
    procedure Respawn;
    procedure SetPos(const APos: TVector3DF; const Rotation: TVector3DF);
    function Cam: TSceneNodeAnimatorCollisionResponse; inline;
  end;

  (*
    CQuake3EventHandler controls the game
  *)
  TQuake3EventHandler = class(TEventReceiver)
  private type
    TParticleImpact = record
      when: U32;
      apos: TVector3DF;
      OutVector: TVector3DF;
    end;
  private
    FGamePtr: PGameData;

    FMesh: TQ3LevelMesh;
    FMapParent: TSceneNode;
    FShaderParent: TSceneNode;
    FItemParent: TSceneNode;
    FUnresolvedParent: TSceneNode;
    FBulletParent: TSceneNode;
    FFogParent: TSceneNode;
    FSkyNode: TSceneNode;
    FMeta: TMetaTriangleSelector;

    FBufStr: AnsiString;

    FImpacts: array of TParticleImpact;

    FGui: TGUI;
  private
    procedure UseItem(APlayer: TQ3Player);
    procedure CreateParticleImpacts(NowTime: U32);
    procedure CreateTextures;
    procedure AddSceneTreeItem(AParent: TSceneNode; NodeParent: TGUITreeViewNode);
  public
    constructor Create(GameDataPtr: PGameData);
    destructor Destroy; override;
  public
    procedure Animate;
    procedure Render;

    procedure AddArchive(const ArchiveName: AnsiString);
    procedure LoadMap(const MapName: AnsiString; Collision: S32);
    procedure CreatePlayers;
    procedure AddSky(Dome: U32; Texture: AnsiString);
    function GetPlayer(Index: U32): TQ3Player;

    procedure CreateGUI;
    procedure SetGUIActive(command: S32);

    function OnEvent(const Event: SEvent): Boolean; override;
  end;


var
  DriverType: E_DRIVER_TYPE;
  Device: TIrrlichtDevice;
  Smgr: TSceneManager;
  Driver: TVideoDriver;
  Camera: TCameraSceneNode;

  TmpStr: String;
  LightRadius: F32 = 60;
  GuiEnv: TGUIEnvironment;
  Skin: TGUISkin;
  Font: TGUIFont;
  ZoneX: F32 = -100;
  ZoneY: F32 = -60;
  ZoneRoot: TSceneNode;
  MeshNode: TMeshSceneNode;
  Rotation: TSceneNodeAnimator;
  Billborad: TBillboardSceneNode;
  MyLightManager: TMyLightManager;

  LastFps: Integer = -1;
  Fps: Integer;

{ TMyLightManager }

constructor TMyLightManager.Create(SceneMgr: TSceneManager);
begin
  FSceneManager := SceneMgr;
end;

// Find the empty scene node that is the parent of the specified node
function TMyLightManager.FindZone(ANode: TSceneNode): TSceneNode;
begin
  if Assigned(ANode) then
  begin
    if ANode.GetType = ESNT_EMPTY then
      Result := ANode
    else
      Result := FindZone(ANode.GetParent);
  end
  else
  begin
    Result := nil;
  end;
end;

function TMyLightManager.OnEvent(const Event: SEvent): Boolean;
begin
  Result := False;

  if (Event.EventType = EET_KEY_INPUT_EVENT) and (Event.KeyInput.PressedDown) then
  begin
    Result := True;
    case Event.KeyInput.Key of
      KEY_KEY_1: FRequestedMode := lmmNoManagement;
      KEY_KEY_2: FRequestedMode := lmmLightsNearestNode;
      KEY_KEY_3: FRequestedMode := lmmLightsInZone;
      else
        Result := False;
    end;

    if FRequestedMode = lmmNoManagement then
      FSceneManager.SetLightManager(nil)
    else
      FSceneManager.SetLightManager(Self);
  end;
end;

// Called after the specified scene node is rendered
procedure TMyLightManager.OnNodePostRender(ANode: TSceneNode);
begin
  // I don't need to do any light management after individual node rendering.
end;

// This is called before the specified scene node is rendered
procedure TMyLightManager.OnNodePreRender(ANode: TSceneNode);
var
  i, lCount: S32;
  ANodePosition: TVector3DF;
  DistanceArray: TArray<TLightDistanceElement>;
  LightNode: TLightSceneNode;
  LightPosition: TVector3DF;
  LightDistance: F64;
  LightData: SLight;
  ParentZone, TmpNode: TSceneNode;
begin
  FCurrentSceneNode := ANode;

  // This light manager only considers solid objects, but you are free to manipulate
  // lights during any phase, depending on your requirements.
  if ESNRP_SOLID <> FCurrentRenderPass then
    Exit;

  // And in fact for this example, I only want to consider lighting for cube scene
  // nodes.  You will probably want to deal with lighting for (at least) mesh /
  // animated mesh scene nodes as well.
  if FMode = lmmLightsNearestNode then
  begin
    // This is a naive implementation that prioritises every light in the scene
    // by its proximity to the node being rendered.  This produces some flickering
    // when lights orbit closer to a cube than its 'zone' lights.
    ANode.GetAbsolutePosition(ANodePosition);

    // Sort the light list by prioritising them based on their distance from the node
    // that's about to be rendered.
    lCount := FSceneLightListWrap.Count;
    SetLength(DistanceArray, lCount);
    for i := 0 to lCount - 1 do
    begin
      LightNode := TLightSceneNode(FSceneLightListWrap.Item[i]^);
      LightNode.GetAbsolutePosition(LightPosition);
      LightDistance := LightPosition.GetDistanceFromSquared(ANodePosition);
      DistanceArray[i] := TLightDistanceElement.Create(LightNode, LightDistance);
    end;

    TArray.Sort<TLightDistanceElement>(DistanceArray, TLightDistanceElement.GetComparer);

    // The list is now sorted by proximity to the node.
    // Turn on the three nearest lights, and turn the others off.
    for i := 0 to lCount - 1 do
      DistanceArray[i].ConcreteNode.SetVisible(i < 3);

    for i := 0 to lCount - 1 do
      DistanceArray[i].Free;
    SetLength(DistanceArray, 0);
  end
  else if FMode = lmmLightsInZone then
  begin
    // Empty scene nodes are used to represent 'zones'.  For each solid mesh that
    // is being rendered, turn off all lights, then find its 'zone' parent, and turn
    // on all lights that are found under that node in the scene graph.
    // This is a general purpose algorithm that doesn't use any special
    // knowledge of how this particular scene graph is organised.
    lCount := FSceneLightListWrap.GetCount;
    for i := 0 to lCount - 1 do
    begin
      TmpNode := FSceneLightListWrap.Item[i]^;

      if TmpNode.GetType <> ESNT_LIGHT then
        Continue;

      LightNode := TLightSceneNode(TmpNode);
      LightData := LightNode.GetLightDataPtr^;

      if LightData.LightType <> ELT_DIRECTIONAL then
        LightNode.SetVisible(False);
    end;

    ParentZone := FindZone(ANode);
    if Assigned(ParentZone) then
      TurnOnZoneLights(ParentZone);
  end;

end;

procedure TMyLightManager.OnPostRender;
var
  i, lCount: S32;
  ANode: TSceneNode;
begin
  // Since light management might be switched off in the event handler, we'll turn all
  // lights on to ensure that they are in a consistent state. You wouldn't normally have
  // to do this when using a light manager, since you'd continue to do light management
  // yourself.
  lCount := FSceneLightListWrap.GetCount;
  for I := 0 to lCount - 1 do
  begin
    ANode := FSceneLightListWrap.Item[i]^;
    ANode.SetVisible(True);
  end;
end;

// This is called before the first scene node is rendered.
procedure TMyLightManager.OnPreRender(LightList: TSceneNodeArray);
begin
  // Update the mode; changing it here ensures that it's consistent throughout a render
  FMode := FRequestedMode;

  // Store the light list. I am free to alter this list until the end of OnPostRender().
  //SceneLightList = lightList;
  FSceneLightListWrap := LightList;
end;

procedure TMyLightManager.OnRenderPassPostRender(
  RenderPass: E_SCENE_NODE_RENDER_PASS);
var
  i, lCount: S32;
  ANode: TSceneNode;
begin
  if ESNRP_SOLID = RenderPass then
  begin
    lCount := FSceneLightListWrap.Count;
    for i := 0 to lCount - 1 do
    begin
      ANode := FSceneLightListWrap.Item[i]^;
      ANode.SetVisible(True);
    end;
  end;
end;

procedure TMyLightManager.OnRenderPassPreRender(
  RenderPass: E_SCENE_NODE_RENDER_PASS);
begin
  // I don't have to do anything here except remember which render pass I am in.
  FCurrentRenderPass := RenderPass;
end;

procedure TMyLightManager.TurnOnZoneLights(ANode: TSceneNode);
var
  NodeListPtr: TSceneNode.PSceneNodeList;
  Child: TNamedObject;
begin
  NodeListPtr := ANode.GetChildren;
  for Child in NodeListPtr^ do
  begin
    if (TSceneNode(Child).GetType = ESNT_LIGHT) then
      TSceneNode(Child).SetVisible(True)
    else  // Assume that lights don't have any children that are also lights
      TurnOnZoneLights(TSceneNode(Child));
  end;

end;

{ TMyLightManager.TLightDistanceElement.TLDEComparer }

function TMyLightManager.TLightDistanceElement.TLDEComparer.Compare(const Left,
  Right: TLightDistanceElement): Integer;
begin
  if Left.FDistance < Right.FDistance then
    Result := -1
  else if Left.FDistance > Right.FDistance then
    Result := 1
  else
    Result := 0;
end;

{ TMyLightManager.TLightDistanceElement }

constructor TMyLightManager.TLightDistanceElement.Create;
begin

end;

constructor TMyLightManager.TLightDistanceElement.Create(Node: TSceneNode;
  d: F64);
begin
  FNode := Node;
  FDistance := d;
end;

class function TMyLightManager.TLightDistanceElement.GetComparer: IComparer<TLightDistanceElement>;
begin
  Result := TLDEComparer.Create;
end;

{ TGameData }

procedure TGameData.Init(const AStartupDir: AnsiString);
begin
  StartupDir := AStartupDir;
  RetVal := 0;
  CreateExDevice := nil;
  Device := nil;

  DebugState := EDS_OFF;
  GravityState := 1;
  FlyTroughState := 0;
  WireFrame := 0;
  GuiActive := 1;
  GuiInputActive := 0;
  GammaValue := 1.0;

  // default deviceParam;
  DeviceParam.Init;

//#if defined ( _IRR_WINDOWS_ )
  DeviceParam.DriverType := EDT_DIRECT3D9;
//#else
//	deviceParam.DriverType = EDT_OPENGL;
//#endif
  DeviceParam.WindowSize.Init(800, 600);
  DeviceParam.FullScreen := False;
  DeviceParam.Bits := 24;
  DeviceParam.ZBufferBits := 16;
  DeviceParam.Vsync := False;
  DeviceParam.AntiAlias := False;

  // default Quake3 loadParam
  LoadParam.Init;
  LoadParam.defaultLightMapMaterial := EMT_LIGHTMAP;
  LoadParam.defaultModulate := EMFN_MODULATE_1X;
  LoadParam.defaultFilter := EMF_ANISOTROPIC_FILTER;
  LoadParam.verbose := 2;
  LoadParam.mergeShaderBuffer := 1;       // merge meshbuffers with same material
  LoadParam.cleanUnResolvedMeshes := 1;   // should unresolved meshes be cleaned. otherwise blue texture
  LoadParam.loadAllShaders := 1;          // load all scripts in the script directory
  LoadParam.loadSkyShader := 0;           // load sky Shader
  LoadParam.alpharef := 1;

  Sound := 0;

  CurrentMapName := '';
  CurrentArchiveList.Clear;

  // Explorer Media directory
  CurrentArchiveList.PushBack(StartupDir + '..\media\');

	// Add the original quake3 files before you load your custom map
	// Most mods are using the original shaders, models&items&weapons
  CurrentArchiveList.PushBack('\q\baseq3\');

  CurrentArchiveList.PushBack(StartupDir + '..\media\map-20kdm2.pk3');

end;

(*
	Load the current game State from a typical quake3 cfg file
*)
function TGameData.Load(const FileName: AnsiString): S32;
var
  Mesh: TQ3LevelMesh;
  EntityList: TEntityArray;
  EntityPtr: PEntity;
  s: AnsiString;
  APos: U32;
  e, g, gs, index, gvs: U32;
  GroupPtr: PVarGroup;
  v: PVariable;
begin
  if not Assigned(Device) then
  begin
    Result := 0;
    Exit;
  end;

  // the quake3 mesh loader can also handle *.shader and *.cfg file
  Mesh := TQ3LevelMesh(Device.GetSceneManager.GetMesh(FileName));
  if not Assigned(Mesh) then
  begin
    Result := 0;
    Exit;
  end;

  EntityList := Mesh.GetEntityDataList;

  e := 0;
  while e < EntityList.Count do
  begin
    EntityPtr := @(EntityList.Item[e]^);
    Inc(e);

    g := 0;
    gs := EntityPtr^.VarGroupList.Count;
    while g < gs do
    begin
      GroupPtr := EntityPtr^.GetGroup(g);
      Inc(g);

      index := 0;
      gvs := GroupPtr^.GetCount;
      while index < gvs do
      begin
        v := GroupPtr^.Items[index];
        Inc(index);

        APos := 0;
        if v.Name = 'playerposition' then
        begin
          PlayerPosition := GetAsVector3DF(v.Context, APos);
        end
        else
        begin
          if v.Name = 'playerrotation' then
          begin
            PlayerRotation := GetAsVector3DF(v.Context, APos);
          end;
        end;
      end;
    end;
  end;

  Result := 1;
end;

(*
	Store the current game State in a quake3 configuration file
*)
function TGameData.Save(const FileName: AnsiString): S32;
begin
  Result := 0;
  //
end;


procedure TGameData.SetDefault;
begin
  DebugState := EDS_OFF;
  GravityState := 1;
  WireFrame := 0;
  GUIActive := 1;
  GUIInputActive := 0;
  GammaValue := 1.0;

  DeviceParam.DriverType := EDT_DIRECT3D9;
  DeviceParam.WindowSize.Width := 800;
  DeviceParam.WindowSize.Height := 600;
  DeviceParam.FullScreen := False;
  DeviceParam.Bits := 24;
  DeviceParam.ZBufferBits := 16;
  DeviceParam.Vsync := False;
  DeviceParam.AntiAlias := False;

  // default Quake3 loadParam
  loadParam.defaultLightMapMaterial := EMT_LIGHTMAP;
  loadParam.defaultModulate := EMFN_MODULATE_1X;
  loadParam.defaultFilter := EMF_ANISOTROPIC_FILTER;
  loadParam.verbose := 2;
	loadParam.mergeShaderBuffer := 1;		  // merge meshbuffers with same material
	loadParam.cleanUnResolvedMeshes := 1;	// should unresolved meshes be cleaned. otherwise blue texture
	loadParam.loadAllShaders := 1;			    // load all scripts in the script directory
	loadParam.loadSkyShader := 0;			    // load sky Shader
	loadParam.alpharef := 1;

  sound := 0;

  CurrentMapName := '';
  CurrentArchiveList.Clear;

  // Explorer Media directory
  CurrentArchiveList.PushBack(StartupDir + '..\..\media\');

	// Add the original quake3 files before you load your custom map
	// Most mods are using the original shaders, models&items&weapons
	CurrentArchiveList.PushBack(StartupDir + '..\..\media\map-20kdm2.pk3');
end;

{ TArchiveListHelper }

procedure TArchiveListHelper.Clear;
begin
  SetLength(Self, 0);
end;

function TArchiveListHelper.GetCount: Integer;
begin
  Result := High(Self) - Low(Self) + 1;
end;

procedure TArchiveListHelper.PushBack(const NewStr: AnsiString);
begin
  SetLength(Self, Count + 1);
  Self[Count - 1] := NewStr;
end;

{ TQ3Player }

function TQ3Player.Cam: TSceneNodeAnimatorCollisionResponse;
begin
  Result := CamCollisionResponse(FDevice);
end;

procedure TQ3Player.CreateIt(ADevice: TIrrlichtDevice; AMesh: TQ3LevelMesh;
  AMapNode: TSceneNode; AMeta: TMetaTriangleSelector);
var
  KeyMap: array[0..9] of SKeyMap;
  WeaponMesh: TAnimatedMeshMD2;
  ASmgr: TSceneManager;
  ADriver: TVideoDriver;
  ACamera: TCameraSceneNode;
  Anim: TSceneNodeAnimator;
  Count, i: S32;
  LogMsg: AnsiString;
  V: TVector3DF;
begin
  SetTimeFire(@FAnim[0], 200, Ord(tffFired));
  SetTimeFire(@FAnim[1], 5000);

  if not Assigned(FDevice) then
    Exit;

  // load FPS weapon to Camera
  FDevice := ADevice;
  FMesh := AMesh;
  FMapParent := AMapNode;

  ASmgr := Device.GetSceneManager;
  ADriver := Device.GetVideoDriver;

  KeyMap[0].Action := EKA_MOVE_FORWARD;
  KeyMap[0].KeyCode := KEY_UP;
  KeyMap[1].Action := EKA_MOVE_FORWARD;
  KeyMap[1].KeyCode := KEY_KEY_W;

	KeyMap[2].Action := EKA_MOVE_BACKWARD;
	KeyMap[2].KeyCode := KEY_DOWN;
	KeyMap[3].Action := EKA_MOVE_BACKWARD;
	KeyMap[3].KeyCode := KEY_KEY_S;

	KeyMap[4].Action := EKA_STRAFE_LEFT;
	KeyMap[4].KeyCode := KEY_LEFT;
	KeyMap[5].Action := EKA_STRAFE_LEFT;
	KeyMap[5].KeyCode := KEY_KEY_A;

	KeyMap[6].Action := EKA_STRAFE_RIGHT;
	KeyMap[6].KeyCode := KEY_RIGHT;
	KeyMap[7].Action := EKA_STRAFE_RIGHT;
	KeyMap[7].KeyCode := KEY_KEY_D;

	KeyMap[8].Action := EKA_JUMP_UP;
	KeyMap[8].KeyCode := KEY_KEY_J;

	KeyMap[9].Action := EKA_CROUCH;
	KeyMap[9].KeyCode := KEY_KEY_C;

  ACamera := ASmgr.AddCameraSceneNodeFPS(nil, 100, 0.6, -1, @KeyMap[0], 10, False, 0.6);
  ACamera.SetName('First Person Camera');
  //ACamera.SetFOV(100 * core::DEGTORAD);
  ACamera.SetFarValue(20000);

  WeaponMesh := TAnimatedMeshMD2(ASmgr.GetMesh('gun.md2'));
  if not Assigned(WeaponMesh) then
    Exit;

  if WeaponMesh.GetMeshType = EAMT_MD2 then
  begin
    Count := WeaponMesh.GetAnimationCount;
    for i := 0 to Count - 1 do
    begin
      LogMsg := 'Animation: ' + AnsiString(WeaponMesh.GetAnimationName(i));
      Device.GetLogger.Log(PAnsiChar(LogMsg), ELL_INFORMATION);
    end;

    FWeaponNode := ASmgr.AddAnimatedMeshSceneNode
                   (
                     WeaponMesh,
                     ASmgr.GetActiveCamera,
                     10,
                     Vector3DF(0, 0, 0),
                     Vector3DF(-90, -90, -90)
                   );
    FWeaponNode.SetMaterialFlag(EMF_LIGHTING, False);
    FWeaponNode.SetMaterialTexture(0, Driver.GetTexture('gun.jpg'));
    FWeaponNode.SetLoopMode(False);;
    FWeaponNode.SetName('tommi the gun man');

    //create a collision auto response animator
    GetGravity('earth', V);
    Anim := ASmgr.CreateCollisionResponseAnimator(AMeta, ACamera, Vector3DF(30, 45, 30),
      V, Vector3DF(0, 40, 0));

    ACamera.AddAnimator(Anim);
    Anim.Drop;

    if Assigned(AMeta) then
      AMeta.Drop;


    Respawn();
    SetAnim('idle');
  end;


end;

procedure TQ3Player.OnAnimationEnd(Node: TAnimatedMeshSceneNode);
begin

end;

(*
	so we need a good starting Position in the level.
	we can ask the Quake3 Loader for all entities with class_name "info_player_deathmatch"
*)
procedure TQ3Player.Respawn;
var
  ACamera: TCameraSceneNode;
  OldPos: Integer;
begin
  if not Assigned(FDevice) then
    Exit;

  ACamera := FDevice.GetSceneManager.GetActiveCamera;

  FDevice.GetLogger.Log('respawn');

  OldPos := FStartPositionCurrent;
  Inc(FStartPositionCurrent);

  if OldPos >= Q3StartPosition(FMesh, ACamera, FStartPositionCurrent, Cam.GetEllipsoidRadius) then
  begin
    FStartPositionCurrent := 0;
  end;
end;

procedure TQ3Player.SetAnim(const Name: PC8);
begin

end;

procedure TQ3Player.SetPos(const APos, Rotation: TVector3DF);
begin

end;

procedure TQ3Player.ShutDown;
var
  ACamera: TCameraSceneNode;
begin
  SetAnim(nil);

  if Assigned(FWeaponNode) then
  begin
    FWeaponNode.Remove;
    FWeaponNode := nil;
  end;

  if Assigned(FDevice) then
  begin
    ACamera := FDevice.GetSceneManager.GetActiveCamera;
    if Assigned(ACamera) then
    begin
      ACamera.Drop;
      FDevice := nil;
    end;
  end;

  FMapParent := nil;
  FMesh := nil;
end;

(*
  The main game states
*)
procedure runGame(game: TGameData);
var
  eventHandler: TQuake3EventHandler;
  i, listSize: S32;
begin
  if game.RetVal >= 3 then
    Exit;

  game.Device := game.CreateExDevice(game.DeviceParam);
  if not Assigned(game.Device) then
  begin
    // could not create selected driver.
    game.RetVal := 0;
    Exit;
  end;

  // create an event receiver based on current game data
  eventHandler := TQuake3EventHandler.Create(@game);

  // load stored config
  game.Load('explorer.cfg');

  // add our media directory and archive to the file system
  listSize := game.CurrentArchiveList.Count;
  for i := 0 to listSize - 1 do
  begin
    eventHandler.AddArchive(game.CurrentArchiveList[i]);
  end;

  // Load a Map or startup to the GUI
  if Length(game.CurrentMapName) > 0 then
  begin
    eventHandler.LoadMap(game.CurrentMapName, 1);
    if game.LoadParam.loadSkyShader = 0 then
      eventHandler.AddSky(1, 'skydome2');
    eventHandler.CreatePlayers;
    eventHandler.CreateGUI;
    eventHandler.SetGUIActive(0);

    // set player to last position on restart
    if game.RetVal = 2 then
    begin
      eventHandler.GetPlayer(0).SetPos(game.PlayerPosition, game.PlayerRotation);
    end;
  end
  else
  begin
    // start up empty
    eventHandler.AddSky(1, 'skydome2');
    eventHandler.CreatePlayers;
    eventHandler.CreateGUI;
    eventHandler.SetGUIActive(1);

    BackgroundMusic('IrrlichtTheme.ogg');
  end;

  game.RetVal := 3;
  while game.Device.Run do
  begin
    eventHandler.Animate;
    eventHandler.Render;
    //if ( !game->Device->isWindowActive() )
      game.Device.Yield;
  end;

  game.Device.SetGammaRamp(1, 1, 1, 0, 0);
  eventHandler.Free;
end;


var
  PrgName, DllName: String;
  game: TGameData;
{ TQuake3EventHandler }

procedure TQuake3EventHandler.AddArchive(const ArchiveName: AnsiString);
var
  fs: TFileSystem;
  i, fc: S32;
  Index: U32;
  exists: Boolean;
  FileList: TFileList;
  PathStrPtr: PIrrAnsiString;
  Archive: TFileArchive;
  TypeName: AnsiString;
  Bank: TGUISpriteBank;
begin
  fs := FGamePtr^.Device.GetFileSystem;

  exists := False;

  fc := S32(fs.GetFileArchiveCount);
  for i := 0 to fc - 1 do
  begin
    FileList := fs.GetFileArchive(i).GetFileList;;
    PathStrPtr := FileList.GetPath;

    if PathStrPtr^ = ArchiveName then
    begin
      exists := True;
      break;
    end;

    if not exists then
    begin
      fs.AddFileArchive(ArchiveName, True, False);
    end;
  end;

	// store the current archives in game data
	// show the attached Archive in proper order
  if Assigned(FGUI.ArchiveList) then
  begin
    FGUI.ArchiveList.ClearRows;

    fc := S32(fs.GetFileArchiveCount);
    for i := 0 to fc - 1 do
    begin
      Archive := fs.GetFileArchive(Cardinal(i));
      Index := FGUI.ArchiveList.AddRow(Cardinal(i));

      case Archive.GetType of
        EFAT_ZIP: TypeName := 'ZIP';
        EFAT_GZIP: TypeName := 'gzip';
        EFAT_FOLDER: TypeName := 'Mount';
        EFAT_PAK: TypeName := 'PAK';
        EFAT_TAR: TypeName := 'TAR';
        else TypeName := 'archive'
      end;

      FGUI.ArchiveList.SetCellText(Index, 0, String(TypeName));
      FGUI.ArchiveList.SetCellText(Index, 1, String(Archive.GetFileList.GetPath^));
    end;
  end;

  // browse the archives for maps
  if Assigned(FGUI.MapList) then
  begin
    FGUI.MapList.Clear;

    Bank := FGamePtr^.Device.GetGUIEnvironment.GetSpriteBank('sprite_q3map');
    if not Assigned(Bank) then
      Bank := FGamePtr^.Device.GetGUIEnvironment.AddEmptySpriteBank('sprite_q3map');

    ...

  end;




end;

procedure TQuake3EventHandler.AddSceneTreeItem(AParent: TSceneNode;
  NodeParent: TGUITreeViewNode);
begin

end;

procedure TQuake3EventHandler.AddSky(Dome: U32; Texture: AnsiString);
		// irrlicht order
		//static const c8*p[] = { "ft", "lf", "bk", "rt", "up", "dn" };
		// quake3 order
const
  op: array[0..5] of AnsiString = ('ft', 'lf', 'bk', 'rt', 'up', 'dn');
var
  ASmgr: TSceneManager;
  ADriver: TVideoDriver;
  OldMipMapState: Boolean;
  i: S32;
begin
  ASmgr := FGamePtr^.Device.GetSceneManager;
  ADriver := FGamePtr^.Device.GetVideoDriver;

  OldMipMapState := ADriver.GetTextureCreationFlag(ETCF_CREATE_MIP_MAPS);
  ADriver.SetTextureCreationFlag(ETCF_CREATE_MIP_MAPS, False);


  case Dome of
    0:
      begin
        FBufStr := Format('%s_%s.jpg', [String(Texture), op[0]]);
        FSkyNode := ASmgr.AddSkyBoxSceneNode(ADriver.GetTexture(FBufStr), nil, nil, nil, nil, nil);

        if Assigned(FSkyNode) then
        begin
          for i := 0 to 5 do
          begin
            FBufStr := Format('%s_%s.jpg', [Texture, op[i]]);
            FSkyNode.GetMaterial(i).SetTexture(0, ADriver.GetTexture(FBufStr));
          end;
        end;
      end;
    1:
      begin
        FBufStr := Format('%s.jpg', [Texture]);
        FSkyNode := ASmgr.AddSkyDomeSceneNode(ADriver.GetTexture(FBufStr), 32, 32,
                      1, 1, 1000, nil, 11);
      end;
    2:
      begin
        FBufStr := Format('%s.jpg', [Texture]);
        FSkyNode := ASmgr.AddSkyDomeSceneNode(ADriver.GetTexture(FBufStr), 16, 8,
                      0.95, 2, 1000, nil, 11)
      end;
  end;

  if Assigned(FSkyNode) then
    FSkyNode.SetName('Skydome');
  //SkyNode->getMaterial(0).ZBuffer = video::EMDF_DEPTH_LESS_EQUAL;

  ADriver.SetTextureCreationFlag(ETCF_CREATE_MIP_MAPS, OldMipMapState);
end;

procedure TQuake3EventHandler.Animate;
begin

end;

constructor TQuake3EventHandler.Create(GameDataPtr: PGameData);
begin
  FGamePtr := GameDataPtr;

  // Also use 16 Bit Textures for 16 Bit RenderDevice
  if FGamePtr^.DeviceParam.Bits = 16 then
  begin
    FGamePtr^.Device.GetVideoDriver.SetTextureCreationFlag(ETCF_ALWAYS_16_BIT, True);
  end;

  // Quake3 Shader controls Z-Writing
  FGamePtr.Device.GetSceneManager.GetParameters.SetAttribute(ALLOW_ZWRITE_ON_TRANSPARENT, True);

  // create internal textures
  CreateTextures;

  SoundInit(FGamePtr^.Device);

  FGamePtr^.Device.SetEventReceiver(Self);
end;

procedure TQuake3EventHandler.CreateGUI;
begin

end;

procedure TQuake3EventHandler.CreateParticleImpacts(NowTime: U32);
begin

end;

procedure TQuake3EventHandler.CreatePlayers;
begin

end;

procedure TQuake3EventHandler.CreateTextures;
var
  ADriver: TVideoDriver;
  Dim: TDimension2DU;
  AImage: TImage;
  i, x, y: U32;
  DataPtr: PU32;
begin
  {$POINTERMATH ON}
  ADriver := FGamePtr^.Device.GetVideoDriver;

  Dim.Init(64, 64);

  for i := 0 to 7 do
  begin
    AImage := ADriver.CreateImage(ECF_A8R8G8B8, Dim);
    DataPtr := AImage.Lock;
    for y := 0 to S32(dim.Height) - 1 do
    begin
      for x := 0 to S32(dim.Width) - 1 do
        DataPtr[x] := $FFFFFFFF;

      DataPtr := PU32(PU8(DataPtr) + AImage.GetPitch);
    end;
    AImage.Unlock;

    FBufStr := Format('smoke_%0.2d', [i]);
    ADriver.AddTexture(FBufStr, AImage);

    AImage.Drop;
  end;

  // fog
  for i := 0 to 0 do
  begin
    AImage := ADriver.CreateImage(ECF_A8R8G8B8, Dim);
    DataPtr := PU32(AImage.Lock);
    for y := 0 to S32(Dim.Height) - 1 do
    begin
      for x := 0 to S32(Dim.Width) - 1 do
      begin
        DataPtr[x] := $FFFFFFFF;
      end;
      DataPtr := PU32(PU8(DataPtr) + AImage.GetPitch);
    end;
    AImage.Unlock;
    FBufStr := Format('fog_%0.2d', [i]);
    ADriver.AddTexture(FBufStr, AImage);
    AImage.Drop;
  end;
end;

destructor TQuake3EventHandler.Destroy;
begin

  inherited;
end;

function TQuake3EventHandler.GetPlayer(Index: U32): TQ3Player;
begin

end;

procedure TQuake3EventHandler.LoadMap(const MapName: AnsiString;
  Collision: S32);
begin

end;

function TQuake3EventHandler.OnEvent(const Event: SEvent): Boolean;
begin

end;

procedure TQuake3EventHandler.Render;
begin

end;

procedure TQuake3EventHandler.SetGUIActive(command: S32);
begin

end;

procedure TQuake3EventHandler.UseItem(APlayer: TQ3Player);
begin

end;

{ TGUI }

procedure TGUI.Drop;
begin
  if Assigned(Self.Window) then
  begin
    Self.Window.Remove;
    Self.Window := nil;
  end;

  if Assigned(Self.Logo) then
  begin
    Self.Logo.Remove;
    Self.Logo := nil;
  end;
end;

procedure TGUI.Init;
begin
  FillChar(Self, SizeOf(Self), 0);
end;

begin
  try
    ReportMemoryLeaksOnShutdown := True;

    PrgName := ParamStr(0);

    game.Init(AnsiString(ExtractFilePath(PrgName)));

    // dynamically load irrlicht
    if ParamCount > 1 then
      DllName := ParamStr(1)
    else
      DllName := 'irrlicht.dll';

    game.CreateExDevice := load_createDeviceEx(AnsiString(DllName));
    //game.CreateExDevice := createDeviceEx;

    if not Assigned(game.CreateExDevice) then
    begin
      game.RetVal := 3;
      Writeln('Could not load ' + DllName + '.');

      ExitCode := game.RetVal;
      Exit;
    end;

    // start without asking for driver
    game.RetVal := 1;
    repeat
      // if driver could not created, ask for another driver
      if game.RetVal = 0 then
      begin
        game.SetDefault;
        // ask user for driver
        game.DeviceParam.DriverType := driverChoiceConsole;
        if game.DeviceParam.DriverType = EDT_COUNT then
          game.RetVal := 3;
      end;

      runGame(game);
    until (game.RetVal < 3);

    ExitCode := game.RetVal;

  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
